#include	"ini.h"
#include	<cstdio>
#include	<cstring>

namespace {
	enum State {
		Success = 0,
		Param,
		FileAccess,
		LostSession,
		LostKey,
		SessionSyntax,
		KeySyntax,
		ValueSyntax,
		BraceMismatch,
		QuoteMismatch,
		EqualMismatch,
	};

	static char * __INISKIP(char * p) {
		if (!p || !(*p)) return p;
		
		char * r = p;
		char c = *r;

		while (c && (c == ' ' || c == '\r' || c == '\n' || c == '\t')) {
			++r;
			c = *r;
		}

		return r;
	}

	static int __INISESSION(char * src, std::string & out) {
		char * p = src;
		bool quote = false;

		while (*p) {
			char ch = *p;

			if (quote) {
				if (ch == '"') {
					if (out.empty()) return State::SessionSyntax;

					p = __INISKIP(p + 1);
					if (!*p) return State::BraceMismatch;
					if (*p != ']') return State::SessionSyntax;

					p = __INISKIP(p + 1);
					if (*p && *p != '#' && *p != ';') return State::SessionSyntax;

					return 0;
				} else {
					out.push_back(ch);
				}
			} else if (ch == '"') {
				if (!out.empty()) return State::SessionSyntax;
				quote = true;
			} else if (ch == '#' || ch == ';' || ch == '[' || ch == '=') {
				return State::SessionSyntax;
			} else if (ch == ' ' || ch == '\t') {
				p = __INISKIP(p + 1);
				if (*p && *p != ']') return State::SessionSyntax;
				continue;
			} else if (ch == ']') {
				if (out.empty()) return State::LostSession;

				p = __INISKIP(p + 1);
				if (*p && *p != '#' && *p != ';') return State::SessionSyntax;
				return 0;
			} else {
				out.push_back(ch);
			}

			++p;
		}

		return State::SessionSyntax;
	}

	static int __INIKEY(char ** src, std::string & out) {
		char * p = *src;
		bool quote = false;

		while (*p) {
			char ch = *p;

			if (quote) {
				if (ch == '"') {
					if (out.empty()) return State::KeySyntax;

					p = __INISKIP(p + 1);
					if (!*p) return State::EqualMismatch;
					if (*p != '=') return State::KeySyntax;

					*src = p + 1;
					return 0;
				} else {
					out.push_back(ch);
				}
			} else if (ch == '"') {
				if (!out.empty()) return State::KeySyntax;
				quote = true;
			} else if (ch == '#' || ch == ';' || ch == '[' || ch == ']') {
				return State::KeySyntax;
			} else if (ch == ' ' || ch == '\t') {
				p = __INISKIP(p + 1);
				if (*p && *p != '=') return State::KeySyntax;
				continue;
			} else if (ch == '=') {
				if (out.empty()) return State::LostKey;
				*src = p + 1;
				return 0;
			} else {
				out.push_back(ch);
			}

			++p;
		}

		return State::KeySyntax;
	}

	static int __INIVALUE(char * src, std::string & out) {
		char * p = src;
		bool quote = false;

		while (*p) {
			char ch = *p;

			if (quote) {
				if (ch == '"') {
					p = __INISKIP(p + 1);
					if (*p && *p != '#' && *p != ';') return State::ValueSyntax;
					return 0;
				} else {
					out.push_back(ch);
				}
			} else if (ch == '"') {
				if (!out.empty()) return State::ValueSyntax;
				quote = true;
			} else if (ch == '[' || ch == ']' || ch == '=') {
				return State::ValueSyntax;
			} else if (ch == '#' || ch == ';') {
				return 0;
			} else if (ch == ' ' || ch == '\t') {
				p = __INISKIP(p + 1);
				if (*p && *p != '#' && *p != ';') return State::ValueSyntax;
				return 0;
			} else {
				out.push_back(ch);
			}

			++p;
		}

		if (quote) return State::QuoteMismatch;
		return 0;
	}
}

bool IniFile::Load(const std::string & path) {
	_error = 0;

	if (path.empty()) {
		_error = State::Param;
		return false;
	}

	FILE * file = NULL;

#if defined(_WIN32)
	if (fopen_s(&file, path.c_str(), "r") != 0) {
#else
	if ((file = fopen(path.c_str(), "r")) == NULL) {
#endif
		_error = State::FileAccess;
		return false;
	}

#define CHECK_FIALED(expr) if ((_error = (expr)) != 0) { fclose(file); return false; }

	/// Skip UTF-8 bomb
	{
		uint8_t bomb[3] = { 0, 0, 0 };
		size_t count = fread(bomb, 1, 3, file);
		if (count < 3 || !(bomb[0] == 0xEF && bomb[1] == 0xBB && bomb[2] == 0xBF)) fseek(file, 0, SEEK_SET);
	}	

	char buf[2048];
	std::string line, session, key, value;	

	while (!feof(file)) {
		::memset(buf, 0, 2048);
		fgets(buf, 2048, file);

		line = Trim(buf);
		if (line.empty()) continue;

		::memset(buf, 0, 2048);
		::memcpy(buf, line.c_str(), line.length());

		char ch = *buf;
		char * p = buf;

		if (ch == '#' || ch == ';') {
			continue;
		} else if (ch == '[') {
			++p;
			session.clear();
			CHECK_FIALED(__INISESSION(p, session));
		} else if (session.empty()) {
			_error = State::LostSession;
			fclose(file);
			return false;
		} else {
			key.clear();
			value.clear();

			CHECK_FIALED(__INIKEY(&p, key));
			p = __INISKIP(p);

			CHECK_FIALED(__INIVALUE(p, value));
			if (value.empty()) continue;
			
			auto it = _sessions.find(session);
			if (it == _sessions.end()) {
				std::map<std::string, std::string> kv;
				kv[key] = value;
				_sessions[session] = kv;
			} else {
				it->second[key] = value;
			}
		}
	}

	fclose(file);
	return true;
}

std::string IniFile::GetError() {
	switch (_error) {
	case State::Success:
		return "Success";
	case State::Param:
		return "Param for IniFile::Load error";
	case State::FileAccess:
		return "Can NOT access gaven file";
	case State::LostSession:
		return "Lost session";
	case State::LostKey:
		return "Lost key";
	case State::SessionSyntax:
		return "Session syntax error";
	case State::KeySyntax:
		return "Key syntax error";
	case State::ValueSyntax:
		return "Value syntax error";
	case State::BraceMismatch:
		return "Brace mismatch";
	case State::QuoteMismatch:
		return "Quote mismatch";
	case State::EqualMismatch:
		return "Equal mismatch";
	default:
		return "Unknown error";
	}
}

std::vector<std::string> IniFile::GetSessions() {
	std::vector<std::string> ret;
	for (auto it = _sessions.begin(); it != _sessions.end(); ++it)
		ret.push_back(it->first);
	return std::move(ret);
}

std::vector<std::string> IniFile::GetKeys(const std::string & sSession) {
	std::vector<std::string> ret;
	
	auto s = _sessions.find(sSession);
	if (s == _sessions.end()) return std::move(ret);

	for (auto k = s->second.begin(); k != s->second.end(); ++k)
		ret.push_back(k->first);

	return std::move(ret);
}
